import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;


public class ScannerGenerator {



	/**
	 * Break a line for a character class down into a string with the id name, class def and optionally superclass
	 * @param line the line
	 * @return a String[] as described above
	 */
	private static String[] tokenizeCharClassLine(String line) {
		int i = 0;
		while(line.charAt(i) != '$') i++;
		int j = i;
		while(!Character.isWhitespace(line.charAt(j))) j++;
		String ID = line.substring(i, j);
		i = j;
		while(line.charAt(i) != '[' && line.charAt(i) != '.') i++;
		if(line.charAt(i) == '.')
			return new String[] {ID, "."};
		j = i;
		while(j < line.length()) {
			if(line.charAt(j) == '\\') {
				j+=2;
			}
			else if(line.charAt(j++) == ']') break;
		}
		String charClass = line.substring(i, j);
		i = j;
		while(i < line.length() && line.charAt(i) != '$' && line.charAt(i) != '[') i++;
		String superClass = null;
		if(i != line.length()) superClass = line.substring(i).trim();

		if(superClass == null) 
			return new String[] {ID, charClass};
		return new String[] {ID, charClass, superClass};
	}

	/**
	 * Given the spec for character classes, build a map of ID=>NFAs.
	 * @param s the spec for character classes
	 * @return a mapping from IDs to NFAs.
	 */
	public static Map<String, NFA> readAndBuildCharClasses(String s) {
		Scanner scan = new Scanner(s);
		Map<String, Set<Character>> tempSet = new HashMap<String, Set<Character>>();
		String line;
		Set<Character> set;

		while(scan.hasNextLine()) {
			line = scan.nextLine();
			if(!line.startsWith("$")) continue;
			String[] test = tokenizeCharClassLine(line);
			if(test.length == 2) {
				tempSet.put(test[0], charClassToSet(test[1]));
			} else {
				set = tempSet.get(test[2]);
				if(set == null) 
					set = charClassToSet(test[2]);
				tempSet.put(test[0], subClassCharClass(test[1], set));
			}
		}
		Map<String, NFA> ret = new HashMap<String, NFA>();
		for(String key : tempSet.keySet()) {
			ret.put(key, charClassToNFA(tempSet.get(key)));

		}
		return ret;
	}

	/**
	 * Take in strings of the type [...] and generates a set of valid characters in the character class
	 * @param str the character class 
	 * @return a set of all valid characters, or null if the character class was not valid (bad escape sequence)
	 */
	public static Set<Character> charClassToSet(String str) {
		Set<Character> list = new HashSet<Character>();
		if(str.trim().startsWith(".")) {  //Deal with . character classes
			for(char i = 0; i < 128; i++) {
				if(i != State.EPSILON)
					list.add(i);
			}
			return list;
		}
		char cur = 0;
		boolean escapeFlag = false, rangeFlag = false, ignoreFlag = true;
		for(int i = 1; i < str.length(); i++) {

			if(!escapeFlag && !rangeFlag && !ignoreFlag) { //If we're not doing something special, add the previous character.
				list.add(cur);
			} 
			ignoreFlag = false;
			if(escapeFlag) {  //If we're in the middle of an escape sequence, grab the current character.
				cur = str.charAt(i);
				if(cur != '\\' && cur != '-' && cur != '[' && cur != ']' && cur != '^') //Invalid escape sequence, return null!
					return null;
				escapeFlag = false;
			} else if(str.charAt(i) == '-') {  //If we see the range operator outside of an escape sequence, set the range flag.
				rangeFlag = true;
			} else if(str.charAt(i) == '\\') {  //If we see an escape operator, set the escape flag
				escapeFlag = true;
			} else if(rangeFlag) { //If the range flag is set, and the current character is not an escape sequence, do the range stuff
				while(cur <= str.charAt(i)) {
					list.add(cur++);
				}
				ignoreFlag = true; //We should ignore the current character the next iteration, we've taken care of it.
				rangeFlag = false;
			} else {
				cur = str.charAt(i);
			}


		}

		return list;
	}

	/**
	 * Given a char class and a description of a subset of that char class,
	 * generate the subclass of that char class.
	 * @param str the subclass description
	 * @param charClass the parent char class
	 * @return the subclass
	 */
	public static Set<Character> subClassCharClass(String str, Set<Character> charClass) {
		int i = str.indexOf("^");
		str = str.substring(0, i).concat(str.substring(i+1, str.length()));
		Set<Character> toRemove = charClassToSet(str);
		Set<Character> ret = new HashSet<Character>();
		ret.addAll(charClass);
		ret.removeAll(toRemove);
		return ret;
	}

	/**
	 * Given a set of valid characters, generate the NFA for that character class
	 * @param characters the characters for the NFA
	 * @return the NFA for the character class
	 */
	public static NFA charClassToNFA(Set<Character> characters) {
		State start = new State(false);
		State accept = new State(true);
		for(Character c : characters) {
			start.addTransition(c, accept);
		}
		return new NFA(start, accept);
	}



	/**
	 * Build an DFA from the given lexical spec
	 * @param fileName the lexical spec
	 * @return the DFA representing the lexical spec
	 * @throws FileNotFoundException if fileName is not a file
	 */
	public static DFA dfaFromLexicalSpec(String fileName) throws FileNotFoundException {
		File f = new File(fileName);
		if(! f.canRead()){
			System.out.println("Cannot read File!");
			if(!f.exists()){
				System.out.println("File does not exist!");
				System.out.println("File path: \n"+f.getPath()+"\n"+f.getAbsolutePath());
			}
		}
		Scanner scan = new Scanner(f);
		String line, charClasses = "", tokens = "";
		int state = 0;
		HashMap<String, Integer> priorityMap = new HashMap<String, Integer>();
		int priority = 0;
		String[] temp;
		while(scan.hasNextLine()) {

			line = scan.nextLine().trim();
			if(line.isEmpty() && state == 0) continue; //White space at the beginning of the file
			if(line.isEmpty() && state == 1) state = 2; //Newline between char classes and token defs


			if(line.startsWith("%") || line.isEmpty()) //Skippable lines
				continue;
			if(state == 1 || state == 0) { //We're in character classes 
				charClasses += line + "\n";
				state = 1;
			} else if(state == 2) { //We've got tokens
				tokens += line + "\n";
				temp = line.split("\\s", 2);
				priorityMap.put(temp[0], priority++);
			}
		}
		DFA ret = new DFA(Parser.parse(tokens, readAndBuildCharClasses(charClasses)), priorityMap);
		return ret;
	}

	/**
	 * Build an DFA from the given lexical spec
	 * @param fileName the lexical spec
	 * @return the DFA representing the lexical spec
	 * @throws FileNotFoundException if fileName is not a file
	 */
	public static DFA dfaFromRegexString(String regex) throws FileNotFoundException {
		File f = new File("regexFormat.txt");
		if(! f.canRead()){
			System.out.println("Cannot read File!");
			if(!f.exists()){
				System.out.println("File does not exist!");
				System.out.println("File path: \n"+f.getPath()+"\n"+f.getAbsolutePath());
			}
		}
		Scanner scan = new Scanner(f);
		String line, charClasses = "", tokens = "";
		int state = 0;
		HashMap<String, Integer> priorityMap = new HashMap<String, Integer>();
		int priority = 0;
		String[] temp;
		while(scan.hasNextLine()) {

			line = scan.nextLine().trim();
			if(line.isEmpty() && state == 0) continue; //White space at the beginning of the file
			if(line.isEmpty() && state == 1) state = 2; //Newline between char classes and token defs


			if(line.startsWith("%") || line.isEmpty()) //Skippable lines
				continue;
			if(state == 1 || state == 0) { //We're in character classes 
				charClasses += line + "\n";
				state = 1;
			} else if(state == 2) { //We've got tokens
				tokens += line + "\n";
				temp = line.split("\\s", 2);
				priorityMap.put(temp[0], priority++);
			}
		}
		//now add the regex to map
		priorityMap.put("$MATCH "+regex, priority++);
		DFA ret = new DFA(Parser.parse(tokens, readAndBuildCharClasses(charClasses)), priorityMap);
		return ret;
	}
	
	/**
	 * Build an NFA from the given lexical spec
	 * @param fileName the lexical spec
	 * @return the NFA representing the lexical spec
	 * @throws FileNotFoundException if fileName is not a file
	 */
	public static NFA nfaFromLexicalSpec(String fileName) throws FileNotFoundException {
		File f = new File(fileName);
		Scanner scan = new Scanner(f);
		String line, charClasses = "", tokens = "";
		int state = 0;
		HashMap<String, Integer> priorityMap = new HashMap<String, Integer>();
		int priority = 0;
		String[] temp;
		while(scan.hasNextLine()) {

			line = scan.nextLine().trim();
			if(line.isEmpty() && state == 0) continue;
			if(line.isEmpty() && state == 1) state = 2;


			if(line.startsWith("%") || line.isEmpty())
				continue;
			if(state == 1 || state == 0) { //We're in character classes 
				charClasses += line + "\n";
				state = 1;
			} else if(state == 2) { //We've got tokens
				tokens += line + "\n";				
				temp = line.split("\\s", 2);
				priorityMap.put(temp[0], priority++);
			}
		}
		NFA n = Parser.parse(tokens, readAndBuildCharClasses(charClasses));
		n.setPriorityMap(priorityMap);
		return n;
	}

	public static void main(String[] args) {
		String spec, file;
		String number = "";
		if(args.length < 2) {
			number = "02";  //The number of the test case to run.  Change this if you want a different test case
			spec = "testSpec" + number + ".txt";
			file = "testFile" + number + ".txt";
		} else {
			spec = args[0];
			file = args[1];
		}
		try {
			DFA dfa = dfaFromLexicalSpec(spec);
			NFA nfa = nfaFromLexicalSpec(spec);
			if(!number.isEmpty()) {
				FileOutputStream fos = new FileOutputStream(new File("testOutputDFA" + number + ".txt"));
				System.setOut(new PrintStream(fos));
			} else {
				System.out.println("*****DFA Results*****");
			}
			dfa.tokenize(new Scanner(new File(file)));
			if(!number.isEmpty()) {
				FileOutputStream fos = new FileOutputStream(new File("testOutputNFA" + number + ".txt"));
				System.setOut(new PrintStream(fos));
			} else {
				System.out.println("*****NFA Results*****");
			}
			nfa.tokenize(new Scanner(new File(file)));

		} catch (FileNotFoundException e) {
			System.out.println("File not found.");
		}
	}



}
